Skip to content

Conversation

@sanny-io
Copy link
Contributor

@sanny-io sanny-io commented Dec 17, 2025

Sample Usage

model User {
    id Int @id
    uuid7 String @default(uuid(7, 'user_uuid7_%s'))
    cuid String @default(cuid(2, 'user_cuid_%s'))
    nanoid String @default(nanoid(21, 'user_nanoid_%s'))
    ulid String @default(ulid('user_ulid_%s'))
}

There are arguments to be made that @default could be a better place to handle this, which would open up opportunities for default values to be composed of complex expressions.

For now, this seems OK.

Summary by CodeRabbit

  • New Features

    • ID generation functions (UUID, CUID, NanoID, ULID) accept an optional format string that may include a "%s" placeholder to embed the generated value.
  • Bug Fixes / Validation

    • Runtime validation now enforces that provided format strings include "%s", returning an error for invalid formats.
  • Tests

    • Added unit and end-to-end tests covering valid/invalid format strings and formatted ID generation across creation scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Walkthrough

Public ID-generator signatures gained an optional format parameter; runtime ID generation now validates and applies a %s format string when provided; language validators and tests were added to enforce and exercise the new format behavior across creation paths.

Changes

Cohort / File(s) Summary
Schema: stdlib model
packages/language/res/stdlib.zmodel
Extended public function signatures to accept an optional format: String? parameter: uuid, cuid, nanoid now accept format, and ulid now accepts format: String?.
Runtime: ID generation formatting
packages/orm/src/client/crud/operations/base.ts
ID generation for cuid, uuid, nanoid, and ulid now passes generated values through a new private formatGeneratedValue(generated, formatExpr?) helper which validates the format expression is a string containing %s and replaces the first %s with the generated value.
Language: invocation validation
packages/language/src/validators/function-invocation-validator.ts
Added a check that for id-like functions (uuid, ulid, cuid, nanoid) a provided format argument must be a literal string containing %s; reports an error otherwise.
Tests: language unit tests
packages/language/test/function-invocation.test.ts
Added tests asserting that id functions accept/no-accept format strings: valid format strings pass, invalid ones produce schema-load errors.
Tests: e2e generated ID formats
tests/e2e/orm/client-api/generated-id-format-strings.test.ts
Added E2E tests that push a SQLite-backed schema exercising uuid/uuid7, cuid/cuid2, nanoid/nanoid8, and ulid across User/Post/Comment models and assert formatted generated IDs for top-level, nested, and deeply nested creates.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review formatGeneratedValue() handling for non-literal or computed expressions and its error messages.
  • Validate correct argument indexing for each generator (e.g., nanoid length vs format).
  • Ensure unit and E2E tests cover absence of format, valid formats, invalid formats, and nested create flows.

Possibly related PRs

Poem

🐰 I hop through code and stitch a tag,
I find the %s and make it brag.
UUIDs, CUIDs, nanoids in line,
ULIDs gleam with the chosen sign.
Hooray — formatted IDs, neat and spry! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding support for format strings to generated identifiers, enabling ID prefixing functionality across uuid, cuid, nanoid, and ulid functions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/e2e/orm/client-api/generated-id-format-strings.test.ts (2)

7-1060: Consider refactoring verbose schema definition.

The schema definition spans over 1000 lines with significant duplication—each field's default value is defined identically in both attributes[0].args[0].value and the default property. While this may be required by the SchemaDef type structure, consider:

  1. Extract a helper function to generate ID field definitions:
function makeIdField(name: string, funcName: string, args: any[], format: string) {
  const callExpr = { kind: 'call', function: funcName, args };
  return {
    name,
    type: 'String',
    attributes: [{ name: '@default', args: [{ name: 'value', value: callExpr }] }],
    default: callExpr
  };
}
  1. This would reduce the ~1050-line schema to ~100-200 lines, improving readability and maintainability.

1062-1167: Solid E2E coverage for core scenarios.

The tests effectively validate ID formatting across top-level, nested, and deeply nested creation scenarios, covering all supported ID generation strategies (uuid, uuid7, cuid, cuid2, nanoid, nanoid8, ulid).

Consider adding test cases for edge cases:

  • Invalid format strings (missing "%s") to verify validation errors
  • Format strings with special characters or unusual patterns
  • Verification that the generated suffix is a valid ID (not just the prefix)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dbe7257 and afe1e7d.

📒 Files selected for processing (3)
  • packages/language/res/stdlib.zmodel (2 hunks)
  • packages/orm/src/client/crud/operations/base.ts (2 hunks)
  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zmodel: Always run zenstack generate after modifying ZModel schemas
ZModel schema files should define database structure and policies that compile to TypeScript via zenstack generate

Files:

  • packages/language/res/stdlib.zmodel
tests/e2e/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

E2E tests should validate real-world schema compatibility with established projects

Files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
🧠 Learnings (7)
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.test.{ts,tsx} : ORM package tests should include comprehensive client API tests and policy tests

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Implement plugin hooks at ORM, Kysely, and entity mutation levels for query interception and customization

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/**/type*.{ts,tsx} : Ensure TypeScript inference and type coverage are validated through type coverage tests

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to **/*.zmodel : ZModel schema files should define database structure and policies that compile to TypeScript via `zenstack generate`

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to packages/zenstackhq/orm/**/*.{ts,tsx} : Use Kysely as the query builder interface for low-level database queries, avoiding raw SQL when possible

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
📚 Learning: 2025-10-21T16:09:31.218Z
Learnt from: ymc9
Repo: zenstackhq/zenstack-v3 PR: 319
File: packages/runtime/src/client/executor/zenstack-query-executor.ts:63-72
Timestamp: 2025-10-21T16:09:31.218Z
Learning: In ZenStack, TypeDefs can be inherited by models. When a TypeDef contains fields with `map` attributes, those mapped field names need to be processed by the QueryNameMapper since they become part of the inheriting model's schema. Therefore, when checking if a schema has mapped names (e.g., in `schemaHasMappedNames`), both `schema.models` and `schema.typeDefs` must be inspected for `@map` and `map` attributes.

Applied to files:

  • tests/e2e/orm/client-api/generated-id-format-strings.test.ts
🧬 Code graph analysis (1)
packages/orm/src/client/crud/operations/base.ts (2)
packages/schema/src/expression-utils.ts (1)
  • ExpressionUtils (19-123)
packages/common-helpers/src/tiny-invariant.ts (1)
  • invariant (4-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)
🔇 Additional comments (4)
packages/language/res/stdlib.zmodel (1)

86-105: LGTM! Consistent API extension for ID formatting.

The optional format parameter has been consistently added to all ID generation functions (uuid, cuid, nanoid, ulid), maintaining backward compatibility while enabling the new prefixing feature.

packages/orm/src/client/crud/operations/base.ts (3)

863-878: LGTM! Consistent integration of format string support.

The formatGeneratedValue helper is correctly integrated into all ID generators:

  • cuid, uuid, nanoid use args[1] for the format parameter (after version/length)
  • ulid correctly uses args[0] since it has no version parameter

The logic properly handles optional formatting while maintaining backward compatibility.


896-912: Clarify single vs. multiple placeholder handling.

The implementation replaces only the first occurrence of "%s" using replace(). The comment at line 910 questions whether replaceAll should be used instead.

Consider:

  1. Should multiple "%s" placeholders be supported (e.g., "prefix_%s_suffix_%s")?
  2. If multiple placeholders aren't intended, should the validation enforce a single "%s"?
  3. The current behavior will leave subsequent "%s" as literal strings, which may be unexpected.

Additionally, consider adding type validation that generated is a string before calling replace(), even though the generators should always return strings.

For design clarity, please confirm the intended behavior and consider either:

  • Using replaceAll('%s', generated) if multiple placeholders should be supported
  • Validating exactly one "%s" exists if single replacement is intended

907-908: Effective input validation.

The invariant checks properly validate that the format is a string and contains the required "%s" placeholder, providing clear error messages if the format is misconfigured.

@sanny-io
Copy link
Contributor Author

sanny-io commented Dec 17, 2025

Looking into the failed tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b3f9f4 and 6ab3c72.

📒 Files selected for processing (1)
  • packages/language/res/stdlib.zmodel (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.zmodel

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.zmodel: Always run zenstack generate after modifying ZModel schemas
ZModel schema files should define database structure and policies that compile to TypeScript via zenstack generate

Files:

  • packages/language/res/stdlib.zmodel
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ab3c72 and 37c3182.

📒 Files selected for processing (1)
  • packages/orm/src/client/crud/operations/base.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/orm/src/client/crud/operations/base.ts (2)
packages/schema/src/expression-utils.ts (1)
  • ExpressionUtils (19-123)
packages/common-helpers/src/tiny-invariant.ts (1)
  • invariant (4-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)
🔇 Additional comments (1)
packages/orm/src/client/crud/operations/base.ts (1)

863-890: Consider explicit version validation for UUID generation.

The implementation handles UUID v7 generation correctly, but silently defaults all other values (including v1, v3, v5, v6, or invalid inputs) to v4. Since the uuid library supports versions 1, 3, 4, 5, 6, and 7, consider either:

  • Supporting all available versions
  • Validating the version parameter and throwing an error for unsupported values
  • Explicitly documenting that only v7 and v4 are supported

@sanny-io
Copy link
Contributor Author

@ymc9 tests are passing and this should be good to go. I debated between replace and replaceAll because I didn't know why anyone would ever want the ID more than once, but opted to use replaceAll because it might be considered an oversight or gotcha.

Let me know what you think and if you'd like more test cases.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/orm/src/client/crud/operations/base.ts (1)

904-910: Add defensive runtime checks for format string validity.

While language-level validation ensures format strings contain %s, the runtime formatter lacks defensive checks. If the validator is bypassed or the schema is inconsistent, a format string without %s would produce non-unique IDs.

Additionally, the current implementation uses replaceAll, which substitutes all occurrences of %s with the same generated ID. Based on the PR examples ('user_uuid7_%s'), single substitution appears to be the primary use case. If multiple %s placeholders are unintended, consider using replace for single substitution or documenting the multiple-substitution behavior.

🔎 Suggested defensive checks
 private formatGeneratedValue(generated: string, formatExpr?: Expression) {
     if (!formatExpr || !ExpressionUtils.isLiteral(formatExpr)) {
         return generated;
     }
+
+    const format = formatExpr.value;
+    invariant(typeof format === 'string', 'generated identifier format value must be a string');
+    invariant(format.includes('%s'), 'generated identifier format strings must include "%s"');

-    return formatExpr.value.replaceAll('%s', generated);
+    // Use replace for single substitution, or document if multiple substitutions are intended
+    return format.replace('%s', generated);
 }
🧹 Nitpick comments (1)
packages/language/src/validators/function-invocation-validator.ts (1)

90-98: LGTM with an optional clarity suggestion.

The validation logic correctly enforces that format strings include %s. The code safely handles cases where the format parameter doesn't exist (formatParamIdx === -1) through optional chaining.

💡 Optional refactor for clarity

Consider making the index check explicit to improve readability:

 if (['uuid', 'ulid', 'cuid', 'nanoid'].includes(funcDecl.name)) {
     const formatParamIdx = funcDecl.params.findIndex(param => param.name === 'format');
-    const formatArg = getLiteral<string>(expr.args[formatParamIdx]?.value);
-    if (formatArg && !formatArg.includes('%s')) {
-        accept('error', 'argument must include "%s"', {
-            node: expr.args[formatParamIdx]!,
-        });
+    if (formatParamIdx >= 0) {
+        const formatArg = getLiteral<string>(expr.args[formatParamIdx]?.value);
+        if (formatArg && !formatArg.includes('%s')) {
+            accept('error', 'argument must include "%s"', {
+                node: expr.args[formatParamIdx],
+            });
+        }
     }
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37c3182 and 4f116e6.

📒 Files selected for processing (3)
  • packages/language/src/validators/function-invocation-validator.ts (1 hunks)
  • packages/language/test/function-invocation.test.ts (1 hunks)
  • packages/orm/src/client/crud/operations/base.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/e2e/**/*.{ts,tsx} : E2E tests should validate real-world schema compatibility with established projects

Applied to files:

  • packages/language/test/function-invocation.test.ts
📚 Learning: 2025-11-26T01:55:04.540Z
Learnt from: CR
Repo: zenstackhq/zenstack-v3 PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:55:04.540Z
Learning: Applies to tests/**/type*.{ts,tsx} : Ensure TypeScript inference and type coverage are validated through type coverage tests

Applied to files:

  • packages/language/test/function-invocation.test.ts
🧬 Code graph analysis (2)
packages/language/src/validators/function-invocation-validator.ts (1)
packages/language/src/utils.ts (1)
  • getLiteral (272-286)
packages/orm/src/client/crud/operations/base.ts (1)
packages/schema/src/expression-utils.ts (1)
  • ExpressionUtils (19-123)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-test (20.x, postgresql)
  • GitHub Check: build-test (20.x, sqlite)
🔇 Additional comments (2)
packages/language/test/function-invocation.test.ts (1)

1-209: LGTM! Comprehensive test coverage.

The test suite thoroughly validates the new format string functionality for ID generation functions:

  • Confirms format strings are optional (backward compatibility)
  • Validates correct handling of format strings containing %s
  • Ensures validation errors for format strings missing %s

All four ID generators (uuid, cuid, nanoid, ulid) are tested with various parameter combinations.

packages/orm/src/client/crud/operations/base.ts (1)

863-886: ID generation logic is correct.

The modifications properly extract optional parameters (version, length) and pass format expressions to the new formatting helper. Parameter positions align with the extended function signatures.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant